#include "../gl/Window.hpp"
#include "../gl/Camera.hpp"
#include "../gl/Sphere.hpp"
#include "../opengl/opengl.hpp"
#include <vector>
#include <fmt/format.h>
#include <glm/glm.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtc/matrix_transform.hpp>

namespace gl = opengl;
PerspectiveCamera camera;

class Object3D {
public:
    Object3D() {}
    Object3D(Object3D &&other) : position(std::move(other.position)) {}
    Object3D(const Object3D &) = delete;
    Object3D &operator=(Object3D &&) = delete;
    Object3D &operator=(const Object3D &) = delete;

public:
    void updateMatrix() {
        modelMatrix = glm::scale(glm::mat4(1.0f), scale);
        modelMatrix = glm::eulerAngleXYZ(rotation.x, rotation.y, rotation.z) * modelMatrix;
        modelMatrix = glm::translate(modelMatrix, position);
        normalMatrix = glm::transpose(glm::inverse(modelMatrix));
    }

public:
    glm::vec3 position{0.0f};
    glm::vec3 scale{1.0f};
    glm::vec3 rotation{0.0f};

public:
    glm::mat4 modelMatrix{1.0f};
    glm::mat4 normalMatrix{1.0f};
};

class Mesh3D : public Object3D {
public:
    struct VertexData {
        std::vector<Vertex> vertices;
        std::vector<gl::u32> indices{};
    };
    struct Material {
        glm::vec3 color{1.0f};
        float shininess{32.0f};
    };

public:
    Mesh3D() {}
    Mesh3D(const VertexData &data, const char *vsFile, const char *fsFile) { create(data, vsFile, fsFile); };
    void create(const VertexData &data, const char *vsFile, const char *fsFile) {
        vao.create({gl::vertex_attribute(0, gl::type_f32, 3, 0), gl::vertex_attribute(1, gl::type_f32, 3, 12)});
        vbo.create(sizeof(Vertex) * data.vertices.size(), data.vertices.data());
        numIndices = data.indices.size();
        ibo.create(4 * numIndices, data.indices.data());
        program.create({gl::shader::from_file(gl::shader::vertex_shader, vsFile),
                        gl::shader::from_file(gl::shader::fragment_shader, fsFile)});
        vao.set_vertex_buffer(vbo, sizeof(Vertex));
        vao.set_index_buffer(ibo);
    };
    void render() {
        updateMatrix();
        program.set_vec3f(program.get_uniform_location("cameraPos"), glm::value_ptr(camera.position));
        program.set_mat4f(program.get_uniform_location("modelMatrix"), glm::value_ptr(modelMatrix));
        program.set_mat4f(program.get_uniform_location("normalMatrix"), glm::value_ptr(normalMatrix));
        program.set_mat4f(program.get_uniform_location("viewProjectionMatrix"),
                          glm::value_ptr(camera.viewProjectionMatrix));
        program.set_vec3f(program.get_uniform_location("material.color"), glm::value_ptr(material.color));
        program.set_float32(program.get_uniform_location("material.shininess"), material.shininess);
        vao.bind();
        program.use();
        gl::api::glDrawElements(gl::mode_triangles, numIndices, gl::type_u32, nullptr);
    }

public:
    Material material{};
    gl::vertex_array vao{};
    gl::buffer vbo{}, ibo{};
    gl::program program{};
    gl::u32 numIndices{0};
};

int main() {
    Window window("Model", 800, 600);
    gl::init_opengl(gl::opengl_config{.load_function = &SDL_GL_GetProcAddress,
                                      .debug_msg_callback = [](auto, auto, auto id, auto, auto, auto msg, auto) {
                                          fmt::println("[OpenGL object {}] {}", id, msg);
                                      }});
    camera.set_position({10.0f, 10.0f, -10.0f});
    camera.look_at(glm::vec3{0.0f});
    window.on_exec = [&](auto) {
        auto [w, h] = window.get_size();
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
        gl::api::glEnable(gl::depth_test);
    };
    window.on_resize = [&](auto, int w, int h) {
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };
    window.on_process_event = [&](auto, const SDL_Event *event) {
        if (event->type == SDL_EventType::SDL_MOUSEWHEEL) {
            constexpr float sensitivity = 2.0f;
            auto fov = glm::clamp(camera.fov - sensitivity * event->wheel.preciseY, 15.0f, 85.0f);
            camera.set_fov(fov);
        }
    };
    Mesh3D plane(Mesh3D::VertexData{.vertices =
                                        {
                                            Vertex{{1, 0, 1}, {0, 1, 0}},
                                            Vertex{{1, 0, -1}, {0, 1, 0}},
                                            Vertex{{-1, 0, -1}, {0, 1, 0}},
                                            Vertex{{-1, 0, 1}, {0, 1, 0}},
                                        },
                                    .indices = {0, 1, 2, 0, 2, 3}},
                 "../shader/depth/pvs.glsl", "../shader/depth/pfs.glsl");
    ;
    Mesh3D sphere;
    {
        Sphere<64> data;
        sphere.create(Mesh3D::VertexData{.vertices{data.vertices, data.vertices + data.num_vertices},
                                         .indices{data.indices, data.indices + data.num_indices}},
                      "../shader/depth/pvs.glsl", "../shader/depth/pfs.glsl");
        // sphere.material.shininess = 4.0f;
        sphere.material.color = {0.f, 1.f, 0.f};
    }
    // plane.position.y
    // plane.rotation.x = glm::radians(45.0);
    plane.updateMatrix();
    fmt::println("{}", plane.program.get_error());
    gl::buffer lightBuffer;

    struct {
        alignas(16) glm::vec3 ambient{0.1f};
        alignas(16) glm::vec3 diffuse{0.7f};
        alignas(16) glm::vec3 specular{0.2f};
        alignas(16) glm::vec3 position{0.f, 2.f, 0.f};
    } light;
    lightBuffer.create(sizeof(light), &light);
    lightBuffer.bind_to(gl::buffer::uniform_buffer, 0);
    // gl::api::glDepthFunc(gl::cmp_greater);
    window.on_render = [&](auto) {
        gl::clear_buffers();
        gl::api::glClear(gl::OPENGL_DEPTH_BUFFER_BIT);
        float t = SDL_GetTicks() / 500.0f;
        light.position = {4 * cos(t), 10.f, 4 * sin(t)};
        lightBuffer.set_data(&light, sizeof(light));
        camera.update_matrix();
        plane.scale = {5, 1, 5};
        sphere.position.y = 3.0f;
        plane.render();
        sphere.render();
    };
    return window.exec();
}